package org.boticelli.logsearch;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.lucene.analysis.StopAnalyzer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.LockObtainFailedException;
import org.boticelli.model.LogEntry;
import org.boticelli.plugin.dist.MessageLogger;
import org.hibernate.Session;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.core.io.Resource;

/**
 * Indexes {@link LogEntry}s. Used by the {@link MessageLogger} and the
 * {@link ReindexController}.
 */
public class LogIndexer
        implements InitializingBean, ApplicationListener
{
    private File indexDir;

    private List<LogEntry> indexQueue = new ArrayList<LogEntry>();

    private long lastFlush = System.currentTimeMillis();

    private long autoFlushInterval = 60 * 60 * 1000;

    /**
     * Sets the auto flush interval in seconds
     * 
     * @param flushInterval
     */
    public void setAutoFlushInterval(int flushInterval)
    {
        this.autoFlushInterval = flushInterval * 1000;
    }

    /**
     * Sets the directory where the index files are kept.
     * 
     * @param indexDir
     * @throws IOException
     */
    @Required
    public void setIndexDir(Resource indexDir) throws IOException
    {
        this.indexDir = indexDir.getFile();
        
        if (!this.indexDir.exists())
        {
            this.indexDir.mkdirs();
        }
        else
        {
            if (!this.indexDir.isDirectory())
            {
                throw new IllegalArgumentException(indexDir + " is not a directory");
            }
        }
    }

    /**
     * Puts the given list of {@link LogEntry}s into the index queue.
     * 
     * @param logs
     */
    private void enqueue(List<LogEntry> logs)
    {
        synchronized (indexQueue)
        {
            indexQueue.addAll(logs);
        }
    }

    /**
     * Indexes the given {@link LogEntry}s.
     * 
     * @param indexWriter
     * @param logs
     * @throws CorruptIndexException
     * @throws IOException
     */
    private void indexList(IndexWriter indexWriter, List<LogEntry> logs)
            throws CorruptIndexException, IOException
    {
        for (LogEntry log : logs)
        {
            indexWriter.addDocument(LogIndexer.createDocument(log));
        }
    }

    /**
     * Keeps indexing the Logs provided by the {@link IndexLogProvider} until it
     * returns <code>null</code>. All indexing happens in a single locked
     * writer. {@link LogEntry}s inserted via #
     * 
     * @param session
     * @param provider
     * @param forceCreate
     */
    public void index(Session session, IndexLogProvider provider, boolean forceCreate)
    {
        try
        {
            IndexWriter indexWriter = null;
            do
            {
                try
                {
                    indexWriter = createIndexWriter(forceCreate);
                }
                catch (LockObtainFailedException e)
                {
                    Thread.sleep(500);
                }
            } while (indexWriter == null);

            List<LogEntry> logs;
            int i = 0;
            while ((logs = provider.getNextBlock()) != null)
            {
                indexList(indexWriter, logs);
                if ((i++ & 31) == 0)
                {
                    indexWriter.flush();
                    session.clear();
                }
            }
            emptyQueue(indexWriter);
            indexWriter.optimize();
            indexWriter.close();
        }
        catch (CorruptIndexException e)
        {
            throw new RuntimeException("Error creating indexer", e);
        }
        catch (IOException e)
        {
            throw new RuntimeException("IO-error creating indexer", e);
        }
        catch (InterruptedException e)
        {
            throw new RuntimeException("interrupted", e);
        }
    }

    public void index(List<LogEntry> logs, boolean forceCreate)
    {
        try
        {
            IndexWriter indexWriter = createIndexWriter(forceCreate);
            indexList(indexWriter, logs);
            emptyQueue(indexWriter);
            indexWriter.optimize();

            long now = System.currentTimeMillis();
            if (now - lastFlush > autoFlushInterval)
            {
                indexWriter.flush();
                lastFlush = now;
            }
            indexWriter.close();
        }
        catch (LockObtainFailedException e)
        {
            enqueue(logs);
        }
        catch (CorruptIndexException e)
        {
            throw new RuntimeException("Error creating indexer", e);
        }
        catch (IOException e)
        {
            throw new RuntimeException("IO-error creating indexer", e);
        }
    }

    /**
     * Empties the indexing queue and flushes the {@link IndexWriter}. Is
     * called by search before searching.
     * 
     * @return <code>false</code> if no flush happened because the index was
     *         locked.
     */
    public boolean flush()
    {
        try
        {
            IndexWriter indexWriter = createIndexWriter(false);
            emptyQueue(indexWriter);
            indexWriter.optimize();
            indexWriter.flush();
            indexWriter.close();
            return true;
        }
        catch (LockObtainFailedException e)
        {
            return false;
        }
        catch (CorruptIndexException e)
        {
            throw new RuntimeException("Error creating indexer", e);
        }
        catch (IOException e)
        {
            throw new RuntimeException("IO-error creating indexer", e);
        }
    }

    /**
     * Returns a {@link IndexReader} for the configured index.
     */
    public IndexReader getReader() throws CorruptIndexException, IOException
    {
        return IndexReader.open(indexDir);
    }

    /**
     * Creates an {@link IndexWriter}
     * 
     * @param createIndex
     *            if <code>true</code>, the index is created
     * @return
     * @throws CorruptIndexException
     * @throws LockObtainFailedException
     * @throws IOException
     */
    private IndexWriter createIndexWriter(boolean createIndex) throws CorruptIndexException,
            LockObtainFailedException, IOException
    {
        IndexWriter indexWriter = new IndexWriter(indexDir, new StopAnalyzer(), createIndex);
        return indexWriter;
    }

    /**
     * Empties the indexing queue and indexes the {@link LogEntry} accumulated
     * in it.
     * 
     * @param indexWriter
     * @throws CorruptIndexException
     * @throws IOException
     */
    private void emptyQueue(IndexWriter indexWriter) throws CorruptIndexException, IOException
    {
        List<LogEntry> copy = null;
        synchronized (indexQueue)
        {
            copy = new ArrayList<LogEntry>(indexQueue);
            indexQueue.clear();
        }
        indexList(indexWriter, copy);
    }

    /**
     * Makes a document for a File.
     * <p>
     * The document has three fields:
     * <ul>
     * <li><code>path</code>--containing the pathname of the file, as a
     * stored, untokenized field;
     * <li><code>modified</code>--containing the last modified date of the
     * file as a field as created by <a
     * href="lucene.document.DateTools.html">DateTools</a>; and
     * <li><code>contents</code>--containing the full contents of the file,
     * as a Reader field;
     */
    public static Document createDocument(LogEntry log)
    {

        // make a new, empty document
        Document doc = new Document();

        doc.add(new Field("id", String.valueOf(log.getId()), Field.Store.YES,
                Field.Index.UN_TOKENIZED));

        doc.add(new Field("modified", DateTools.timeToString(log.getMoment().getTime(),
                DateTools.Resolution.MINUTE), Field.Store.YES, Field.Index.UN_TOKENIZED));

        doc.add(new Field("contents", log.getIdent() + " " + log.getText(), Field.Store.NO,
                Field.Index.TOKENIZED));

        return doc;
    }

    public void afterPropertiesSet() throws Exception
    {
        // Create an index if there is none
        if (indexDir.list().length == 0)
        {
            createIndexWriter(true);
        }
    }

    public void onApplicationEvent(ApplicationEvent applicationEvent)
    {
        if (applicationEvent instanceof ContextClosedEvent)
        {
            this.flush();
        }
    }
}
